Skip to content
掌握度

CodeAgent CLI 项目深挖 · 单期模块。总览见 需求分析 与 整体设计方案。

Skill 系统解决一个问题——怎么把「Agent 该怎么思考」沉淀成可复用的决策手册,按需喂给模型,又不撑爆每轮重发的 system prompt。

Skill 三层加载与惰性加载 ​

1. 数据模型与三层加载(Skill + SkillRegistry)。 一个 Skill 就是一个目录,根文件必须是 SKILL.md(决策手册),可选 references/(按需读的资料库)、scripts/(可执行依赖)。加载按固定顺序扫三层目录:jar 内置 < 用户级 ~/.codeagent/skills/ < 项目级 <project>/.codeagent/skills/,同名后者覆盖前者,而且覆盖是整体替换不是字段 merge——避免「用户改了 description 但 references 还指向 builtin 缓存路径」这种诡异状态。

2. frontmatter 手写解析(SkillFrontmatterParser)。 刻意不引 SnakeYAML,手写一个极简 YAML 子集:单行 key: value、多行 key: |、行内数组 [a,b,c]。因为 frontmatter 就 5 个字段、全是 string 或 list-of-string,手写 80 行够用,引依赖要多带 90KB jar。未知字段忽略、嵌套对象报 warning 但不阻塞,保前向兼容。

3. 启用状态持久化(SkillStateStore)。 只持久化 disabled 列表到 ~/.codeagent/skills.json,启用是隐式默认——这样新加的 skill 不会被遗漏。文件缺失或解析失败一律视为空 disabled,不阻塞启动。

4. 轻量索引(SkillIndexFormatter)。 启动期只把每个启用 skill 的 name + description 渲染成 system prompt 末尾的索引段,带三重预算:单 description ≤ 500 codepoint、启用数 ≤ 20、总段 ≤ 4096 字节,命中即截断 + stderr 警告。用 codepoint 而不是 String.length(),避免切坏中文和 emoji。

5. 惰性加载(load_skill → SkillContextBuffer)。 这是核心。LLM 看到索引段里某个 description 匹配当前任务,就主动调 load_skill(name);工具校验 name 在启用表里 → 读 SKILL.md 正文(去 frontmatter,5KB 截断)→ push 进 buffer → 工具结果只返回一句「已加载」。下一轮构造 user message 时,把 buffer 里的 body 前置成 ## 已加载 Skill:<name> 段。不是全量塞 system prompt,是按需取用。

6. buffer 生命周期。 三条硬约束:drain() 一次性消费(防跨轮重复注入)、同一会话最多保留 3 个 skill body(超出 LRU 淘汰最旧)、/clear 复位。主 Agent 和各 SubAgent 角色各持独立实例,避免角色间提示词污染。

【主动抛深度点 1:为什么 body 注入 user message 而不是 system prompt】 这是整个 Skill 系统最关键的架构决策,三个理由叠加:一是 system prompt 在多轮对话里只发一次,但 skill 加载是每轮可能不同的,放 user message 才跟得上;二是 user message 每轮重发,注入的 body 会随轮次自然衰减(配合 buffer 只保留最近一次,不堆积);三是 system prompt 不变 → prompt cache 命中率高,把会变的东西放 user message,才能保住 cache 这条线。

【主动抛深度点 2:为什么 load_skill 工具结果只返回「已加载」不直接返回正文】 因为注入位置决定模型怎么理解这段文字。如果走工具结果直接返回正文,模型会把它当「事实输入」——是查回来的资料;而放进下一轮 user message 前置,模型会把它当「用户附加要求」——决策权重更高。Skill 本质是「指令」不是「资料」,所以必须走 user message 而不是 tool result。这就是为什么设计成「工具只回确认、正文走 buffer 延迟注入」的两段式。

三层覆盖具体怎么实现?整体替换还是字段 merge? ​

SkillRegistry 用一个 Map<String, Skill> 存结果,按 builtin → user → project 顺序扫目录,同名直接 put 覆盖。所以是整体替换:用户级 skill 完整顶掉内置版,不存在「用户改了 description、references 还指向 builtin 缓存」的半覆盖状态。要区分来源看 source 字段,/skill list 里会显示 builtin/user/project。

为什么不用 SnakeYAML? ​

投入产出不划算。frontmatter 就 name / description / version / author / tags 五个字段,全是 string 或 list-of-string,手写一个 80 行的 YAML 子集解析器够覆盖 95% 实际写法;引 SnakeYAML 要多带约 90KB jar 体积。不支持嵌套对象/anchor/复杂类型,命中就打 warning 跳过该字段,不阻塞整个 skill 加载。

为什么只持久化 disabled 列表,不持久化 enabled? ​

为了新 skill 不被遗漏。如果持久化 enabled 白名单,以后内置或用户新增一个 skill,默认不在白名单里,就永远「关着」。反过来只存 disabled,默认全开,新加的 skill 自动生效。这也是很多「默认允许 + 显式排除」设计的通用取舍。

索引段为什么要三重预算? ​

因为索引段进的是每轮都带的 system prompt,是常驻 token 成本。三重预算是三个维度的失控防护:单条 description 太长(≤500)防某个 skill 占太多、skill 太多(≤20)防枚举爆炸、总量太大(≤4KB)兜底。超了宁可截断 + 警告,也不能让 skill 索引悄悄吃掉 window。

buffer 一次性消费怎么理解?不一次性会怎样? ​

drain() 取出所有已积累的 body 并清空,拼进下一轮 user message。如果不清空,同一份 skill body 会每轮都前置,第一轮之后就成了永久污染、token 无限累积。一次性消费保证「加载一次、注入一次、用完即走」,模型再想用同 skill 得重新调 load_skill——所以提示词里专门写了「同一会话内一次足够,不要重复加载」。

Skills 的 token 经济学 ​

SkillRegistry.reload 三层扫描 builtin < user < project,同名整体覆盖。注入策略是两级:system prompt 只放 name+description 的索引段(≤4KB);命中后 load_skill 把 SKILL.md 正文截 5KB 推进 SkillContextBuffer(一次性 drain、LRU 限 3 个),下一轮前置到 user message。把「往 system prompt 堆规则」变成「按需加载专家手册」——几十个 Skill 也不撑爆上下文。

讲讲你的 Skills 有哪些? ​

Skill 本质上是可复用的 Agent 决策手册。它不直接执行操作,而是告诉模型在特定场景下应该采用什么工作流、如何选择工具,以及需要遵守哪些约束。

当前项目内置了一个 web-access Skill,主要负责联网搜索和浏览器操作的决策。它会指导 Agent 根据页面类型选择 web_search、web_fetch 或浏览器 MCP;遇到 SPA、防爬页面、登录态页面时如何降级;以及什么时候应该读取 DOM、什么时候才需要截图。

除了内置 Skill,系统还支持用户级和项目级自定义 Skill,因此可以继续扩展代码审查、故障排查、测试生成或者团队开发规范等能力。对于这三级skill,系统先加载内置 Skill,再加载用户级 Skill,项目级 Skill。同名时后加载的覆盖前面的,因此项目可以针对自己的技术栈和团队规范覆盖默认行为,而不需要修改程序源码。

Skill 如何加载? ​

Skill 采用渐进式披露,分为索引、正文和参考资料三个层次。

第一层是索引。启动时只把已启用 Skill 的名称和描述放进 System Prompt。最多注入 20 个 Skill,整个索引控制在约 4KB,单条描述最多 500 个 Unicode code point。模型可以用很低的上下文成本知道当前有哪些能力。

第二层是 Skill 正文。当模型判断某个 Skill 与当前任务匹配时,会调用 load_skill(name)。系统找到对应的 SKILL.md,把正文截断到最多 5KB,然后写入当前 Agent 独立的 SkillContextBuffer。下一条用户消息进入时,正文会被前置到用户输入之前。

缓冲区最多暂存三个 Skill。重复加载同名 Skill 会刷新顺序,超过三个时淘汰最早进入且未被刷新的内容。正文注入后缓冲区会被一次性清空,避免每轮重复注入;已经注入的正文会作为 User 消息的一部分留在对话历史中,直到历史被压缩或执行 /clear。

第三层是参考资料。一个 Skill 目录下可以包含 references/,正文只描述在什么条件下需要读取哪份资料。例如 web-access 会根据目标网站,按需读取对应站点的访问经验,而不是启动时把所有站点规则一起加载。

为什么不一次性全量加载? ​

第一是控制上下文占用。大多数任务只会使用一两个 Skill,如果全量加载,很多与当前任务无关的指令会长期占据上下文窗口。

第二是降低 Token 成本。即使稳定前缀能够命中 Prompt Cache,缓存读取通常也不是完全免费,而且首次请求仍然需要处理全部内容。

第三是减少指令冲突。不同 Skill 可能包含不同的流程和工具偏好,全量注入会增加模型选择错误规则或被无关指令干扰的概率。

所以我只常驻一个轻量索引,让模型根据任务按需加载正文,再由正文决定是否读取更细的参考资料。这类似操作系统的分页思想:先保留目录,真正使用时再加载具体内容。

八股延伸|Skill 的渐进式加载机制了解吗 ​

Skill 的渐进式加载机制了解吗

“了解,核心就八个字,索引常驻,正文按需。具体分两段。”

“第一段,每次用户输入时,只把匹配度最高的 5 个 Skill 的名字和描述注入上下文,单条描述截断到 300 字符,整个索引不超过 4000 字符。这时模型知道有哪些技能可用,但一个字的正文都没进来。”

“第二段,模型判断当前任务匹配某个 Skill,主动调用加载工具,这时才读 SKILL.md 的正文,上限 5000 字符。而且正文不是立刻塞进当前轮,是放进一个缓冲区,下一轮随工具结果一起注入,缓冲区只保留最近 3 条,防止越积越多。”

“Skill 目录本身也分三层,内置的、用户级的、项目级的,同名时项目级覆盖用户级,和记忆文件的分层逻辑一致。”

“这套机制的收益可以量化。20 个 Skill 按单篇 5000 字符的上限全量加载,就是 10 万字符;渐进式披露的常驻成本只有 4000 字符的索引,差 25 倍。”

候选是怎么匹配出来的? ​

“加权打分。用户显式点名某个 Skill 直接给最高分;否则按命中位置算,名字命中的权重最高,标签次之,描述最低,中文还做了二元、三元分词来提升召回。”

“说白了就是一个微型搜索引擎,检索对象从网页换成了技能。”

八股延伸|Skill 分层体系是怎么做的,为什么这么设计? ​

Skill 分层体系是怎么做的,为什么这么设计?

“三层加载,按优先级从低到高:内置 Skill 打包在程序里、用户级 Skill 放在 ~/.codeagent/skills/ 目录、项目级 Skill 放在项目根目录的 .codeagent/skills/ 目录。同名 Skill,优先级高的整体覆盖低的。”

“每个 Skill 就是一个目录,核心是 SKILL.md 文件,用 frontmatter 声明名称、描述、版本、标签,正文就是给 LLM 看的决策手册。可选的 references/ 目录放参考资料,scripts/ 放可执行脚本。”

“为什么这么分?”

内置 Skill 提供开箱即用的基础能力,比如 web 访问的浏览器策略手册。

用户级满足个人工作流定制,比如我自己写了一个小红书热榜抓取的 Skill。

项目级承载团队约定,比如代码审查规范,提交到仓库后团队所有人共享。

这个思路和 Claude Code 的设计一脉相承,Claude Code 也是内置 Skill、用户 Skill、项目 Skill 三层。

八股延伸|用户输入怎么和 Skill 匹配?有积累机制吗? ​

用户输入怎么和 Skill 匹配?有积累机制吗?

“打分制。每个 Skill 的得分由四个维度加权计算。精确名称匹配直接加 10000 分,确保用户点名要的 Skill 一定排第一。名称中的词项命中权重 ×12,标签词项 ×6,描述词项 ×2。”

“分词策略分中英文两套。”

英文转小写后按空格分词,过滤掉 and、for、the 这些停用词。

“中文不做传统分词,而是取单个汉字加上 2 字和 3 字的滑动窗口,这样能覆盖大部分中文短语。比如‘代码审查’会生成‘代’‘码’‘审’‘查’‘代码’‘码审’‘审查’‘代码审’‘码审查’这些特征。”

Skill 只能开发者预定义吗?还是用户也可以自己写? ​

“用户可以在 ~/.codeagent/skills/ 下创建自己的 Skill 目录,写一个 SKILL.md 就能生效。”

启用状态持久化在 skills.json 里,记录的是 disabled 列表,默认全部启用,用 /skill off 命令禁用。

“目前没做自动积累,比如根据用户操作习惯自动生成 Skill。主要是自动生成的决策手册质量不可控,一份写得不好的手册反而会误导 LLM 的判断。”

本站仅供个人学习使用,请勿外传